﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
using TechTree.Stalk.Roots.Cache;
using TechTree.Stalk.Roots.Common;
using TechTree.Stalk.Roots.Exceptions;

namespace TechTree.Stalk.Roots.Cache
{
    public class Cache
    {
        #region Atributes
        private static bool _enableCache;
        private static Dictionary<String, Object> _cacheRepository;
        private static Cache _cache;
        #endregion

        #region Constants
        private const string MESSAGE_ERROR_INITIALIZE_CACHE = "Unable to initialize cache.";
        private const string MESSAGE_ERROR_CACHE_KEY_TYPE = "Unable to create the cache key from type '{0}'.";
        private const string MESSAGE_ERROR_KEYS = "The set of keys should have more than one element.";
        private const string MESSAGE_ERROR_EXISTING_KEYS = "The value could not be stored because the key already exists.";
        private const string MESSAGE_ERROR_KEY_NULL_EMPTY = "The key can not be null or empty.";

        private const string CACHE_KEY_PROP_NAME = "key_";
        #endregion

        #region Properties
        /// <summary>
        /// Gets Cache instance.
        /// </summary>
        public static Cache Instance
        {
            get
            {
                if (_cache == null)
                {
                    _cache = new Cache();
                }

                return _cache;
            }
        }

        /// <summary>
        /// Gets the total amount of cached objects.
        /// </summary>
        public int Count
        {
            get
            {
                if (_enableCache)
                {
                    if (_cacheRepository != null)
                    {
                        return _cacheRepository.Count;
                    }
                }

                return 0;
            }
        }

        /// <summary>
        /// Gets Cache Repository instance.
        /// </summary>
        private IDictionary<String,Object> CacheRepository
        {
            get
            {
                if (_cacheRepository == null)
                {
                    try
                    {
                        if (_enableCache)
                        {
                            _cacheRepository = new Dictionary<String, Object>();
                        }
                    }
                    catch (Exception ex)
                    {
                        throw new CacheException(MESSAGE_ERROR_INITIALIZE_CACHE, ex);
                    }
                }

                return _cacheRepository;
            }
        }
        #endregion

        #region Constructor
        private Cache()
        {
            try
            {
                //TODO: Checar
                _enableCache = true;  //Convert.ToBoolean(ConfiguracaoParametrizacao.ObterParametro(key_PARAMETRIZACAO_HABILITA_CACHE));
            }
            catch (Exception ex)
            {
                _enableCache = false;
            }
        }
        #endregion

        #region Public Methods

        /// <summary>
        /// Verifies if the key exists in cache
        /// </summary>
        /// <param name="key_">The key to be tested.</param>
        /// <returns>'True' if exists. Otherwise 'false'.</returns>
        public bool HasKey(string key_)
        {
            return HasKey(new CacheKey(key_));
        }
        /// <summary>
        /// Verifies if the key exists in cache
        /// </summary>
        /// <param name="key_">The key to be tested.</param>
        /// <returns>'True' if exists. Otherwise 'false'.</returns>
        public bool HasKey(CacheKey key_)
        {
            if (_enableCache)
            {
                if (key_ != null)
                {
                    if (CacheRepository != null)
                    {
                        return CacheRepository.ContainsKey(key_.ToString());
                    }

                    return false;
                }
                else
                {
                    throw new ArgumentNullException(CACHE_KEY_PROP_NAME, MESSAGE_ERROR_KEY_NULL_EMPTY);
                }
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Cleans the cache.
        /// </summary>
        public void Clear()
        {
            if (_enableCache)
            {
                CacheRepository.Clear();
            }
        }

        /// <summary>
        /// Removes an item from the cache.
        /// </summary>
        /// <param name="key_">The key of the object to be removed.</param>
        public void Remove(string key_)
        {
            RemoveObject(new CacheKey(key_));
        }

        /// <summary>
        /// Removes an item from the cache.
        /// </summary>
        /// <param name="key_">The key of the object to be removed.</param>
        public void Remove(CacheKey key_)
        {
            RemoveObject(key_);
        }

        /// <summary>
        /// Retrieves a cached object from its associated key.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <returns>If found returns the value stored in the typed in T. Otherwise its default value.</returns>
        public T Get<T>(string key_)
        {
            return Get<T>(new CacheKey(key_));
        }

        /// <summary>
        /// Retrieves a cached object from its associated key.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="getClone_">True if needs to return a cloned instance. Otherwise, false.</param>
        /// <returns>If found returns the value stored in the typed in T. Otherwise its default value.</returns>
        public T Get<T>(string key_, bool getClone_)
        {
            return Get<T>(new CacheKey(key_), getClone_);
        }

        /// <summary>
        /// Retrieves a cached object from its associated key.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <returns>If found returns the value stored in the typed in T. Otherwise its default value.</returns>
        public T Get<T>(CacheKey key_)
        {
            return Get<T>(key_, false);
        }

        /// <summary>
        /// Retrieves a cached object from its associated key.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="getClone_">True if needs to return a cloned instance. Otherwise, false.</param>
        /// <returns>If found returns the value stored in the typed in T. Otherwise its default value.</returns>
        public T Get<T>(CacheKey key_, bool getClone_)
        {
            if (_enableCache)
            {
                
                T objetoCacheado = GetObject<T>(key_);

                if (getClone_ && (objetoCacheado != null))
                {
                    //Clona o objeto e o retorna.
                    return Tools.Clone<T>(objetoCacheado);
                }

                //Retorna o objeto.
                return objetoCacheado;
            }
            else
            {
                return default(T);
            }
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        public void Include<T>(string key_, T object_)
        {
            Include<T>(new CacheKey(key_), object_);
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        public void Include<T>(CacheKey key_, T object_)
        {
            if (_enableCache)
            {
                Include(key_, object_, 0, false, false);
            }
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        public void Include<T>(string key_, T object_, bool includeClone_)
        {
            Include<T>(new CacheKey(key_), object_, 0, false, includeClone_);
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        /// <param name="includeClone_">Includes a clone of the object_</param>
        public void Include<T>(CacheKey key_, T object_, bool includeClone_)
        {
            if (_enableCache)
            {
                Include(key_, object_, 0, false, includeClone_);
            }
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        /// <param name="expiration_">The expiration time in seconds.</param>
        public void Include<T>(string key_, T object_, long expiration_)
        {
            Include<T>(new CacheKey(key_), object_, expiration_, false, false);
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        /// <param name="expiration_">The expiration time in seconds.</param>
        public void Include<T>(CacheKey key_, T object_, long expiration_)
        {
            if (_enableCache)
            {
                Include(key_, object_, expiration_, false, false);
            }
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        /// <param name="includeClone_">Includes a clone of the object_</param>
        /// <param name="expiration_">The expiration time in milisseconds.</param>
        public void Include<T>(string key_, T object_, bool includeClone_, long expiration_)
        {
            Include<T>(new CacheKey(key_), object_, expiration_, false, includeClone_);
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        /// <param name="includeClone_">Includes a clone of the object_</param>
        /// <param name="expiration_">The expiration time in milisseconds.</param>
        public void Include<T>(CacheKey key_, T object_, bool includeClone_, long expiration_)
        {
            if (_enableCache)
            {
                Include(key_, object_, expiration_, false, includeClone_);
            }
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        /// <param name="expiration_">The expiration time in milisseconds.</param>
        /// <param name="refresh_">Boolean value that defines whether the cached value should be updated.</param>
        public void Include<T>(string key_, T object_, long expiration_, bool refresh_)
        {
            Include<T>(new CacheKey(key_), object_, expiration_, refresh_, false);
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        /// <param name="expiration_">The expiration time in milisseconds.</param>
        /// <param name="refresh_">Boolean value that defines whether the cached value should be updated.</param>
        public void Include<T>(CacheKey key_, T object_, long expiration_, bool refresh_)
        {
            if (_enableCache)
            {
                Include<T>(key_, object_, expiration_, false, false);
            }
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        /// <param name="expiration_">The expiration time in milisseconds.</param>
        /// <param name="refresh_">Boolean value that defines whether the cached value should be updated.</param>
        /// <param name="includeClone_">Includes a clone of the object_</param>
        public void Include<T>(string key_, T object_, long expiration_, bool refresh_, bool includeClone_)
        {
            Include<T>(new CacheKey(key_), object_, expiration_, refresh_, includeClone_);
        }

        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        /// <param name="expiration_">The expiration time in milisseconds.</param>
        /// <param name="refresh_">Boolean value that defines whether the cached value should be updated.</param>
        /// <param name="includeClone_">Includes a clone of the object_</param>
        public void Include<T>(CacheKey key_, T object_, long expiration_, bool refresh_, bool includeClone_)
        {
            if (_enableCache)
            {
                IncludeObject<T>(key_, object_, expiration_, refresh_, includeClone_);
            }
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Removes an item from a cache key.
        /// </summary>
        /// <param name="key_">The key of the object to be removed.</param>
        private void RemoveObject(CacheKey key_)
        {
            if (_enableCache)
            {
                if (key_ != null)
                {
                    if (CacheRepository.ContainsKey(key_.Valor))
                    {
                        CacheRepository.Remove(key_.Valor);
                    }
                }
                else
                {
                    throw new ArgumentNullException(CACHE_KEY_PROP_NAME, MESSAGE_ERROR_KEY_NULL_EMPTY);
                }
            }
        }
        /// <summary>
        /// Retrieve object from a key.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <returns>If found returns the value stored in the typed in T. Otherwise its default value.</returns>
        private T GetObject<T>(CacheKey key_)
        {
            if (_enableCache)
            {
                if (key_ != null)
                {
                    if (!String.IsNullOrEmpty(key_.Valor))
                    {
                        if (CacheRepository.ContainsKey(key_.Valor))
                        {
                            return (T)CacheRepository[key_.Valor];
                        }
                    }
                }
                else
                {
                    throw new ArgumentNullException(CACHE_KEY_PROP_NAME, MESSAGE_ERROR_KEY_NULL_EMPTY);
                }
            }

            return default(T);
        }
        /// <summary>
        /// Inserts the object in the cache.
        /// </summary>
        /// <typeparam name="T">The type of the stored object.</typeparam>
        /// <param name="key_">The key of the object to be removed.</param>
        /// <param name="object_">The object to be stored.</param>
        /// <param name="expiration_">The expiration time in milisseconds.</param>
        /// <param name="refresh_">Boolean value that defines whether the cached value should be updated.</param>
        /// <param name="includeClone_">Includes a clone of the object_</param>
        private void IncludeObject<T>(CacheKey key_, T object_, long expiration_, bool refresh_, bool includeClone_)
        {
            if (_enableCache)
            {
                if (key_ != null)
                {
                    if (CacheRepository.ContainsKey(key_.Valor))
                    {
                        if (refresh_)
                        {
                            RemoveObject(key_);
                        }
                        else
                        {
                            throw new CacheException(MESSAGE_ERROR_EXISTING_KEYS);
                        }
                    }

                    if (includeClone_)
                    {
                        object_ = Tools.Clone<T>(object_);
                    }

                    if (expiration_ == 0)
                    {
                        CacheRepository.Add(key_.Valor, object_);
                    }
                    else
                    {
                        CacheRepository.Add(key_.Valor, object_);

                        CacheTimer timer = new CacheTimer();
                        timer.CachedKey = key_;
                        timer.Interval = expiration_;
                        timer.Elapsed += timer_Elapsed;
                        timer.Enabled = true;
                        timer.AutoReset = false;
                        timer.Start();
                    }

                }
                else
                {
                    throw new ArgumentNullException(CACHE_KEY_PROP_NAME, MESSAGE_ERROR_KEY_NULL_EMPTY);
                }
            }
        }

        /// <summary>
        /// Event triggered after the interval timer is complete.
        /// </summary>
        /// <param name="sender">The timer.</param>
        /// <param name="e">The event args.</param>
        private void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            CacheTimer timer = (CacheTimer)sender;

            if (timer != null)
            {
                CacheKey cachedKey = timer.CachedKey;

                this.RemoveObject(cachedKey);
                timer.Elapsed -= timer_Elapsed;
            }
        }
        #endregion
    }
}
